#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include "common/http_manager.h"
#include "utils/string_utils.h"
#include "utils/file_utils.h"
#include "utils/io_utils.h"
#include "utils/object_utils.h"
#include "basics.h"

#define CONTENT_DISPOSITION_KEY "Content-disposition:"
#define CONTENT_DISPOSITION_KEY_LENGTH 20 //`Content-Disposition:`一共20个字符
#define FILENAME_KEY "filename="
#define FILENAME_KEY_LENGTH 9

RequestContext *CreateRequestContext(Request *request, const char *url, const char *directory) {
  CREATE_OBJECT_CLEANED(RequestContext, context); //RequestContext context = calloc(1, sizeof(RequestContext))
  context->request = request;
  context->url = strdup(url);
  context->download_directory = strdup(directory);
  context->file_size = 0;
  context->is_running = 0; //为1 表示请求正在发送; 为0 表示还没有发起请求

  //用于断点续传
  context->range_start = -1;
  context->range_end = -1;

  return context;
}

void DestroyRequestContext(RequestContext *context) {
  if (context) {
    free(context->url);
    free(context->download_directory);
    free(context->output_filename); // 如果响应头里没有带信息, 就从请求的url里的最后一个segment作为文件名

    if (context->output_stream) {
      fclose(context->output_stream);
    }
    free(context);
  }
}

//https://curl.se/libcurl/c/CURLOPT_PROGRESSFUNCTION.html
/**int progress_callback(void *clientp,   double dltotal,   double dlnow,   double ultotal,   double ulnow);
We encourage users to use the newer CURLOPT_XFERINFOFUNCTION instead, if you can.

这个函数由libcurl调用，而不是使用频繁间隔调用内部等价函数。当数据正在传输时，它会被频繁调用，而在没有任何数据传输的缓慢时期，它可能会慢到大约每秒一次调用。

clientp是用CURLOPT_PROGRESSDATA设置的指针，libcurl不使用它，只是从应用程序传递到回调函数。

回调函数会被告知libcurl将要传输和已经传输了多少数据(以字节为单位)。
dltotal是libcurl期望在此传输中下载的字节总数。
dlnow是目前下载的字节数。
ultotal是libcurl期望在此传输中上传的总字节数。
ulnow是目前上传的字节数。

Unknown/unused argument 传递给回调函数将被设置为0(就像如果你只下载数据,上传大小仍将是0)。
很多次回调将首先被称为一次或多次,才知道数据的大小 所以必须做出一个程序来处理。
Many times the callback will be called one or more times first, before it knows the data sizes so a program must be made to handle that.

如果你的回调函数返回CURL_PROGRESSFUNC_CONTINUE，它将导致libcurl继续执行默认的进度函数。

从这个回调函数返回任何其他非零值都会导致libcurl中止传输并返回CURLE_ABORTED_BY_CALLBACK。

如果您使用multi接口传输数据，这个函数将不会在空闲期间被调用，除非您调用了适当的libcurl函数来执行传输。

CURLOPT_NOPROGRESS必须设置为0，才能真正调用这个函数

By default, libcurl has an internal progress meter. That's rarely wanted by users.
*/
static int CurlProgressFunction(
  RequestContext *context,
  double download_total,
  double download_now,
  double upload_total,
  double upload_now
) {
  puts("---CurlProgressFunction---");
  int result = CURLE_OK;
  if (download_total > 0 && context->response_code > 0) {
    context->request->progress_callback(context->request->receiver,
                                        download_now,
                                        download_total); //如果是断点续传, 这个total是抛开之前已经下载的部分了的

    if (!context->is_running) {
      PRINTLNF("Paused: download: %.2f%%(%.0f/%.0f)",
               download_now * 100 / download_total,
               download_now,
               download_total);

      result = CURLE_ABORTED_BY_CALLBACK; //curl中只要返回这个 就会取消下载
    }
  }
  return result;
}

//Content-disposition 是 MIME 协议的扩展，MIME 协议指示 MIME 用户代理如何显示附加的文件。
//Content-disposition 可以控制用户请求所得的内容存为一个文件的时候提供一个默认的文件名
static int RetrieveOutputNameFromContentDisposition(RequestContext *request_context,
                                                    char const *content_disposition,
                                                    size_t length) {
  /* Example
   * Content-Disposition: filename=name1367; charset=funny; option=strange
   * 这里传进来的content_disposition 就是 `filename=name1367; charset=funny; option=strange` 这部分*/
  /* If filename is present */
  content_disposition = strncasestr(content_disposition, FILENAME_KEY, length); //查找 content_disposition 中 filename=xxx 的部分(虽然我们传进来的就是 filename=name1367... 打头开始的这部分了, 但http协议里 `Content-Disposition:` 和`filename=`之间的空格 可能有多个... 故这里还是找了一下)
  if (!content_disposition) {
    return RESULT_FAILURE;
  }

  content_disposition += FILENAME_KEY_LENGTH; // 指针挪到 filename=name1367 的 name1367打头处
  length -= FILENAME_KEY_LENGTH;

  if (*content_disposition == '"') {
    content_disposition++;
    length--;
  }

  char output_filename[length];
  int i = 0;
  while (i < length && content_disposition[i] != '\r' /*http协议里 换行为`\r\n` 这里只用识别`\r`即可*/
      && content_disposition[i] != ';'
      && content_disposition[i] != '\0'
      && content_disposition[i] != '"') {
    output_filename[i] = content_disposition[i];
    i++;
  }

  output_filename[i] = '\0';
  request_context->output_filename = strdup(output_filename);
  return RESULT_OK;
}

static int RetrieveOutputNameFromUrl(RequestContext *context) {
  int result = RESULT_OK;

  char output_filename[1024];
  char const *url = context->url;
  // http://www.bennyhuo.com/testdata/三国演义.txt?param=1
  // http://www.bennyhuo.com/testdata/三国演义.txt#chapter1
  // url编码 http协议里 字符是有一个范围的 不能什么字符都进来, 你整一个汉字进来 它会被当做一个个普通字符, 比如说ASCII字符
  // http://www.bennyhuo.com/testdata/%E4%B8%89%E5%9B%BD%E6%BC%94%E4%B9%89.txt?param=1
  url = strrchr(url, '/');
  url++;

  int i = 0;
  while (*url != '\0' && *url != '?' && *url != '#') {
    output_filename[i++] = *url++;
  }

  output_filename[i] = '\0';
  char *url_decoded_filename = curl_easy_unescape(context->curl, output_filename, 0, NULL);
  context->output_filename = strdup(url_decoded_filename);
  curl_free(url_decoded_filename);
  return result;
}

//https://curl.se/libcurl/c/CURLOPT_HEADERFUNCTION.html
/** size_t header_callback(char *buffer,   size_t size,   size_t nitems,   void *userdata);
libcurl一收到头数据就会调用这个函数。
报头回调将为每个报头调用一次，并且只有完整的报头行被传递给回调。
使用它解析头非常容易。
buffer points指向交付的数据，
该数据的大小是items;
size总是1。

不要假设header line是空结束的!
Do not assume that the header line is null-terminated!

The pointer named userdata is the one you set with the `CURLOPT_HEADERDATA` option.

这个回调函数必须返回实际处理的字节数。如果该值与传入函数的值不同，它将向库发出错误信号。这将导致传输中止，正在进行的libcurl函数将返回CURLE_WRITE_ERROR。

传递给这个函数的完整HTTP头可以达到CURL_MAX_HTTP_HEADER (100K)字节，并包括最后的行结束符。

如果没有设置此选项，或者将其设置为NULL，
但 `CURLOPT_HEADERDATA` 设置为不是NULL的任何值，则将使用用于接受响应数据的函数。
也就是说，它将使用CURLOPT_WRITEFUNCTION指定的函数，或者如果CURLOPT_WRITEFUNCTION也指定或为NULL, 则将使用默认的流写入函数(stream-writing function fwrite)。

重要的是要注意，在发起请求后，将为收到的所有响应的头调用回调函数，而不仅仅是最终响应。这包括在身份验证协商(authentication negotiation)期间发生的所有响应。如果您只需要对最终响应的头进行操作，那么您将需要自己在回调中收集头，并使用HTTP状态行来划分响应边界

对于HTTP传输，状态行和响应主体前面的空行都包含在报头中，并传递给此函数。

当服务器发送一个块编码传输(chunked encoded transfer)时，它可能包含一个 trailer。这个trailer 与HTTP头相同，如果接收到这样的尾部(trailer)，它也会使用这个回调传递给应用程序。
有几种方法可以检测它是否是一个尾部(trailer)，而不是一个普通的头文件:
1)它出现在响应体之后。
2)它出现在最后的标题行(CR LF)之后
3)a Trailer: header among the regular response-headers mention what header(s) to expect in the trailer.

对于FTP、POP3、IMAP和SMTP等非http协议，该函数将通过libcurl发送的命令的服务器响应被调用。
For non-HTTP protocols like FTP, POP3, IMAP and SMTP this function will get called with the server responses to the commands that libcurl sends.

libcurl does not unfold HTTP "folded headers" (deprecated since RFC 7230). A folded header is a header that continues on a subsequent line and starts with a whitespace. Such folds will be passed to the header callback as a separate one, although strictly it is just a continuation of the previous line.
*/
size_t CurlHeaderFunction(char *buffer, size_t size, size_t nitems, RequestContext *request_context) {
  puts("---CurlHeaderFunction---");

  size_t buffer_size = size * nitems;
  printf("%.*s", (int) buffer_size, buffer);

  //Content-Disposition: filename=name1367; charset=funny; option=strange
  if (buffer_size >= CONTENT_DISPOSITION_KEY_LENGTH &&
      !strncasecmp(buffer, CONTENT_DISPOSITION_KEY, CONTENT_DISPOSITION_KEY_LENGTH)/*strncasecmp 字符比较相等返回的是0*/) {
    int result_code = RetrieveOutputNameFromContentDisposition(
        request_context,
        buffer + CONTENT_DISPOSITION_KEY_LENGTH,
        buffer_size - CONTENT_DISPOSITION_KEY_LENGTH);

    if (result_code == RESULT_FAILURE) {
      PRINTLNF("error: bad remote name");
    }
  }

  return buffer_size;
}

//https://curl.se/libcurl/c/CURLOPT_WRITEFUNCTION.html
/** size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
一旦收到需要保存的数据，libcurl就会调用这个回调函数。
对于大多数传输，这个回调会被多次调用，而且每次调用都会传递另一个数据块。
 ptr指向交付的数据，
 该数据的大小为nmemb;
 size总是1。

回调函数将在所有调用中传递尽可能多的数据，但您一定不要做任何假设。它可能是一个字节，也可能是数千个字节。
传递给write回调函数的最大body数据量在curl.h头文件中定义:CURL_MAX_WRITE_SIZE(通常默认值为16K)。
 If CURLOPT_HEADER is enabled, which makes header data get passed to the write callback, you can get up to CURL_MAX_HTTP_HEADER bytes of header data passed into it. This usually means 100K.

This function may be called with zero bytes data if the transferred file is empty.

The data passed to this function will not be null-terminated!

你的回调函数应该返回实际处理的字节数。如果该值与传递给回调函数的值不同，它将向库发出一个错误条件信号。这将导致传输中止，所使用的libcurl函数将返回CURLE_WRITE_ERROR。

如果你的回调函数返回CURL_WRITEFUNC_PAUSE，它将导致这个传输变得暂停。请参阅curl_easy_pause了解更多细节。

将此选项设置为NULL，以获得用于代替回调的内部默认函数。内部默认函数将数据写入CURLOPT_WRITEDATA给出的文件FILE *中。

libcurl will use 'fwrite' as a callback by default.

这个选项不启用HSTS，您需要使用CURLOPT_HSTS_CTRL来实现这一点。*/
//第一个参数是下载回来的数据.
//最后一个参数是你指定 `curl_easy_setopt(curl, CURLOPT_WRITEDATA/*写到哪里里*/, target);` 时填的那个target指针
//302 跳转不会调用此函数
size_t CurlBodyFunction(char *buffer, size_t size, size_t nitems, RequestContext *request_context) {
  puts("---CurlBodyFunction---");
  PRINTLNF("size: %lld",size);
  PRINTLNF("nitems: %lld",nitems);

  if (!request_context->output_stream) {
    if (!request_context->output_filename) {
      RetrieveOutputNameFromUrl(request_context);
    }

    if (request_context->output_filename) {
      curl_easy_getinfo(request_context->curl, CURLINFO_RESPONSE_CODE, &request_context->response_code);

      //如果是206 表示支持断点续传
      if (request_context->response_code == 206) {
        request_context->output_stream = SmartFOpenFileInDirectory(
            request_context->download_directory,
            request_context->output_filename,
            "ab");
      } else {
        request_context->output_stream = SmartFOpenFileInDirectory(
            request_context->download_directory,
            request_context->output_filename,
            "wb");
      }

      if (!request_context->output_stream) {
        PRINTLNF("Failed to open file: %s/%s",
                 request_context->download_directory,
                 request_context->output_filename);
        return -1;
      }
    } else {
      return -1;
    }
  }

  if (request_context->output_stream) {
    fwrite(buffer, size, nitems, request_context->output_stream);
  }

  return size * nitems;
}

void SendRequest(RequestContext *request_context) {
  if (request_context->is_running) {
    PRINTLNF("request already running ....");
    return;
  }

  CURL *curl = curl_easy_init(); //curl有两套api 另外一套一般用不着
  if (!curl) { //只会在内存不够的情况下出现
    request_context->curl_code = -1;
    return;
  }

  //所有参数都通过 curl_easy_setopt 配置
  curl_easy_setopt(curl, CURLOPT_URL, request_context->url); //配置发送的curl
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1); //追踪重定向 (浏览器都会自动重定向,但curl程序并不会)


  /*CURLcode curl_easy_setopt(CURL *handle, CURLOPT_NOBODY, long opt);
    一个设置为1的long opt(default 0, the body is transferred)告诉libcurl在执行需要下载的操作时，不要在输出中包含body部分。对于HTTP，这会使libcurl执行一个HEAD请求。对于大多数其他协议来说，这意味着不要求传输身体数据。

    For HTTP operations when CURLOPT_NOBODY has been set, unsetting the option (with 0) will make it a GET again - only if the method is still set to be HEAD. The proper way to get back to a GET request is to set CURLOPT_HTTPGET and for other methods, use the POST or UPLOAD options.
    对于已经设置了CURLOPT_NOBODY的HTTP操作，取消该选项(0)将使其再次成为GET——仅当该方法仍然设置为HEAD时。返回get请求的正确方法是设置CURLOPT_HTTPGET，对于其他方法，使用POST或UPLOAD选项。

    启用CURLOPT_NOBODY意味着请求没有正文的下载。

    If you do a transfer with HTTP that involves a method other than HEAD, you will get a body (unless the resource and server sends a zero byte body for the specific URL you request).
  */
  curl_easy_setopt(curl, CURLOPT_NOBODY, request_context->header_only); //请求只想要header, 我们在创建一个下载任务的时候呢, 第一步就是先去解析它的文件名, 它的文件大小, 以及判断它是否支持断点续传, 把这个任务先创建出来, 信息先写到数据库里边, 那么这个过程呢, 我们就需要一个header-only那么一个请求来完成

  //CURLOPT_HEADERFUNCTION
  //这个选项要给一个回调函数作为参数,回调函数相当于一个触发器
  //curl 会把每一条 response 的 header 传给这个函数
  //, 由函数来决定如何执行后面的步骤。
  //如果想让 curl 继续下载后续内容, 那么回调函数应该返回 header 字符串的长度：
  //如果不想继续下载, 直接返回数字 0, 那么 curl 就会发送 TCP 的终止连接的指令, 不会继续下载后续内容了。
  curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, CurlHeaderFunction);
  curl_easy_setopt(curl, CURLOPT_HEADERDATA, request_context);

  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION/*写入文件用哪个函数*/, CurlBodyFunction); //处理请求体的
  curl_easy_setopt(curl, CURLOPT_WRITEDATA/*写到哪里里*/, request_context);

  if (request_context->request->progress_callback) {
    curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L); //默认不会显示进度 除非你这里手动设置 0 将其关掉
    curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, CurlProgressFunction); //指定的函数正常情况下每秒被libcurl调用一次.
    curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, request_context);
  }

  char content_range[40];
  //请求一个文件的中间某部分数据
  if (request_context->range_start >= 0 && request_context->range_end >= 0) {
    //9223372036854775808-9223372036854775808 | 838TB
    snprintf(content_range, 40, "%llu-%llu", request_context->range_start, request_context->range_end);

  //请求一个文件某点以后所有
  } else if (request_context->range_start >= 0) {
    //9223372036854775808- | 838TB
    snprintf(content_range, 40, "%llu-", request_context->range_start);

  //请求一个文件前n个字节(一般用不到)
  } else if (request_context->range_end >= 0) {
    //-9223372036854775808 | 838TB
    snprintf(content_range, 40, "-%llu", request_context->range_end);
  } else {
    // no range request.
    content_range[0] = 0;
  }

  if (content_range[0] > 0) {
    PRINTLNF("content-range: %s", content_range);
    CURLcode code = curl_easy_setopt(curl, CURLOPT_RANGE, content_range); //断点续传, 指定传输分片, 格式:"0-200"
  }

  curl_easy_setopt(curl, CURLOPT_CAINFO, "cacert.pem");
  curl_easy_setopt(curl, CURLOPT_CAPATH, "cacert.pem");
  //↑ 发布时 需要将证书 和 生成的.exe 放在同一目录下
  //  在CLion中点击运行测试时 要设置working directory为 F:\c_workplace\project\gtk-downloader\src

  request_context->curl = curl;
  request_context->is_running = 1;
  puts("---request start---");
  request_context->curl_code = curl_easy_perform(curl); //真正发起请求
  puts("---request end---");
  //调用过程中 会阻断下面, 也就说 走到这里一定是已经请求完了
  request_context->curl = NULL;
  request_context->is_running = 0;

  curl_easy_getinfo(curl, CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, &request_context->file_size);
  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &request_context->response_code);

  if (request_context->header_only && !request_context->output_filename) {
    RetrieveOutputNameFromUrl(request_context);
  }

  if (request_context->output_stream) {
    fclose(request_context->output_stream);
    request_context->output_stream = NULL;
  }

  curl_easy_cleanup(curl);
}


